Skip to content

Fix bug where a local audio track unpublish would not cause the corresponding remote audio track's AudioStreams to stop iterating#672

Draft
1egoman wants to merge 8 commits into
mainfrom
fix-audio-stream-held-open
Draft

Fix bug where a local audio track unpublish would not cause the corresponding remote audio track's AudioStreams to stop iterating#672
1egoman wants to merge 8 commits into
mainfrom
fix-audio-stream-held-open

Conversation

@1egoman
Copy link
Copy Markdown
Contributor

@1egoman 1egoman commented May 28, 2026

Warning

Right now, this pull request is based on top of another one here, and ties into some of this pull request's new infrastructure.

Before an eventual merge, this needs to be cleaned up. But keep this in mind when reviewing - the changes which are specific to this change cleanly are segmented into 28da338 and c31a6ee.

Problem

When a remote peer unpublishes an audio track, a subscriber consuming the resulting AudioStream via for await (const frame of stream) hangs indefinitely - the loop never receives an end-of-stream and the process can't exit. Calling iterator.return() or stream.cancel() to force termination doesn't help either - the in flight promise returned by reader.read() never resolves.

Root cause

The native FFI side only emits eos on the audio stream when one of: (a) the stream's own FFI handle is disposed, (b) the track's FFI handle is disposed, or (c) the underlying libwebrtc receiver gracefully ends. None of these reliably fire on a remote unsubscribe - the SDK keeps the track handle alive (the consumer may still want to read track.sid etc.), and (according to a LLM) the libwebrtc receiver typically keeps the stream "open" with silence rather than ending it.

On the SDK side, AudioStreamSource.cancel() previously disposed the FFI handle but never closed the ReadableStream controller. The native eos (if it ever arrived) would have triggered the close - but since the event listener was removed at the top of cancel(), even a late eos was silently dropped. So any pending reader.read() had nothing to resolve it.

Fix

Two changes in packages/livekit-rtc/src/:

  1. audio_stream.ts: collapse the three cleanup paths ('eos' event, consumer cancel(), and a new closeFromTrack() entry point) into one teardown() method that closes the ReadableStreamDefaultController first. Closing the controller synchronously resolves any pending reader.read() with {done: true}, so for await / iterator.return() unblock without relying on the native eos arriving.

  2. track.ts: when a Track is detached from its Room (the natural signal that no more frames will arrive on its streams), call closeFromTrack() on each registered AudioStream. This gives consumers a clean end-of-stream even when the native layer doesn't emit one.

TODO

  • Add new tests specifically for this case
  • More through manual testing

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: c31a6ee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@livekit/rtc-node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@1egoman 1egoman force-pushed the fix-audio-stream-held-open branch from 3e273e0 to c31a6ee Compare May 29, 2026 19:45
@1egoman 1egoman requested a review from lukasIO May 29, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant